feat: ingestão de contribuições do GitHub e retrospectiva da comunidade#304
feat: ingestão de contribuições do GitHub e retrospectiva da comunidade#304Clintonrocha98 wants to merge 33 commits into
Conversation
Substitui a retrospectiva provisória (comando que rodava `gh` na mão e subia um JSON para um link temporário) por uma ingestão persistente que respeita a arquitetura modular do projeto. - integration-github: modelo próprio de contribuições (desacoplado de Interaction/gamification), lake bruto de eventos espelhando o do Discord, backfill REST resiliente e ciente de rate limit (helper RateLimit compartilhado), e webhook ao vivo com verificação de assinatura HMAC. - panel-admin: allowlist de repositórios gerenciável + ação "Backfill agora" com notificações amigáveis (rate limit / falha) sem marcar sucesso falso. - portal: página pública de retrospectiva por período (Livewire), contando todos os contribuidores, excluindo bots e distinguindo PRs mergeados dos fechados sem merge. A gamificação fica como seam (evento GithubContributionRecorded), sem dependência direta entre os módulos.
O painel admin é multi-tenant, então tratar a allowlist e as contribuições
como globais quebrava ao paginar (o model não tinha relação `tenant`). Agora
cada comunidade tem o seu recorte, consistente com o resto do sistema.
- github_repositories e github_contributions carregam tenant_id; as uniques
passam a (tenant_id, full_name) e (tenant_id, repo, type, external_ref).
- ProjectGithubEvent faz fan-out: uma entrega do webhook vira uma contribuição
por tenant que acompanha o repo; o lake github_event_logs segue global.
- BackfillRepository::execute recebe o GithubRepository e carimba o tenant_id
do repo de origem.
- A retrospectiva do portal filtra por tenant, resolvido por slug de rota
(/comunidade/{tenant}/retrospectiva) com default em config('he4rt.main_tenant').
- GithubRepositoryResource volta a ser tenant-scoped; a validação de unicidade
do form é escopada ao tenant atual.
Contribuição de repo compartilhado é duplicada por tenant — trade-off aceito em
favor do isolamento total entre comunidades.
dda959b to
fb4ee60
Compare
…ps coloridos, descrições)
…com LED ao fundo dos slides
…cair na janela semanal
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a new Integration GitHub module: DB schemas, Eloquent models/factories, enums/DTOs, idempotent recorder and event, Saloon transport requests with PAT auth, a backfill service + CLI command with rate-limit handling, webhook signature middleware, webhook controller and projector, admin Filament resource and actions, portal retrospective read model, Livewire page and deck UI/styles, tests covering backfill/webhooks/retrospective, and docs/config entries including GITHUB_API_TOKEN and GITHUB_WEBHOOK_SECRET. Possibly related PRs
Suggested reviewers
|
There was a problem hiding this comment.
Actionable comments posted: 16
🧹 Nitpick comments (6)
app-modules/portal/src/Retrospective/CommunityRetrospective.php (1)
36-54: ⚡ Quick winPush filterable clauses to the query layer.
The query loads all contributions by tenant and date range into memory, then filters in PHP. For repos, types, and person, these can be pushed to SQL to reduce memory usage and improve performance when the data set grows.
♻️ Proposed refactor to push filters to query
/** `@var` Collection<int, GithubContribution> $contributions */ - $contributions = GithubContribution::query() + $query = GithubContribution::query() ->where('tenant_id', $this->tenantId) - ->whereBetween('occurred_at', [$this->filters->since, $this->filters->until]) - ->get() + ->whereBetween('occurred_at', [$this->filters->since, $this->filters->until]); + + if ($this->filters->repos !== []) { + $query->whereIn('repo', $this->filters->repos); + } + + if ($this->filters->types !== []) { + $query->whereIn('type', $this->filters->types); + } + + if ($this->filters->person !== null) { + $query->where('actor_login', $this->filters->person); + } + + $contributions = $query->get() ->when( $this->filters->hideBots, fn (Collection $items): Collection => $items->reject(fn (GithubContribution $contribution): bool => $this->isBot($contribution)), ) - ->when( - $this->filters->repos !== [], - fn (Collection $items): Collection => $items->filter(fn (GithubContribution $contribution): bool => in_array($contribution->repo, $this->filters->repos, true)), - ) - ->filter(fn (GithubContribution $contribution): bool => in_array($contribution->type, $this->filters->types, true)) ->reject(fn (GithubContribution $contribution): bool => $this->filteredOutByOutcome($contribution)) - ->when( - $this->filters->person !== null, - fn (Collection $items): Collection => $items->filter(fn (GithubContribution $contribution): bool => $contribution->actor_login === $this->filters->person), - ) ->values();app-modules/integration-github/tests/Feature/GithubRepositoryTest.php (1)
26-29: ⚡ Quick winRemove implicit factory dependency in cross-tenant uniqueness coverage.
Line 27 and Line 28 assume separate tenants via factory defaults. Create explicit tenant A/B and attach each repository to keep this test deterministic.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app-modules/integration-github/tests/Feature/GithubRepositoryTest.php` around lines 26 - 29, The test "permite o mesmo full_name em tenants diferentes" relies on implicit tenant separation from GithubRepository::factory()->create(), so make it deterministic by explicitly creating two Tenant instances (e.g., $tenantA and $tenantB) and pass/associate each to the repository factory calls (set tenant_id or use a factory state like forTenant) so the two GithubRepository::factory()->create([...]) calls create records under different tenants; update the test to attach the first create to $tenantA and the second to $tenantB while keeping the same full_name.app-modules/integration-github/tests/Feature/GithubContributionTest.php (1)
39-45: ⚡ Quick winMake tenant separation explicit in this cross-tenant test.
Line 40 and Line 43 depend on factory defaults to create different tenants, which makes the test contract implicit and brittle. Bind each record to explicit tenants in this test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app-modules/integration-github/tests/Feature/GithubContributionTest.php` around lines 39 - 45, The test creates two GithubContribution records relying on factory defaults to produce different tenants; make tenant separation explicit by creating or fetching two distinct tenant entities and passing them into each factory call (e.g., set a tenant_id or associate a Tenant model) when calling GithubContribution::factory()->create([...]) so the two creates reference distinct tenants while keeping repo, type (ContributionType::Pr) and external_ref the same.app-modules/integration-github/database/migrations/2026_06_04_000001_create_github_repositories_table.php (1)
15-15: ⚡ Quick winClarify foreign key deletion behavior.
The
tenant_idforeign key uses the defaultRESTRICTon delete. If a tenant is deleted, this will prevent deletion unless all their GitHub repositories are removed first. If cascade deletion is intended, add->cascadeOnDelete(). If restrict is correct, consider->restrictOnDelete()for explicitness.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app-modules/integration-github/database/migrations/2026_06_04_000001_create_github_repositories_table.php` at line 15, The migration currently defines the foreign key as $table->foreignUuid('tenant_id')->constrained('tenants') which leaves delete behavior as the DB default (RESTRICT); update the migration in create_github_repositories_table to make the intended behavior explicit: if repositories should be removed when a tenant is deleted, change the constraint to use ->cascadeOnDelete() on the tenant_id foreign key, otherwise append ->restrictOnDelete() to document the explicit restrict behavior.app-modules/integration-github/database/migrations/2026_06_04_000002_create_github_contributions_table.php (1)
13-32: ⚖️ Poor tradeoffMissing index for repository-scoped queries.
The retrospective filtering (per stack context) allows filtering by repository within a tenant. Queries like
WHERE tenant_id = ? AND repo = ? ORDER BY occurred_atwill perform a non-covering index scan. Add a composite index on(tenant_id, repo, occurred_at)to support efficient repo-scoped temporal queries.📊 Proposed index addition
$table->index(['tenant_id', 'occurred_at'], 'idx_github_contributions_tenant_time'); $table->index('actor_id', 'idx_github_contributions_actor'); $table->index(['tenant_id', 'type', 'occurred_at'], 'idx_github_contributions_type_time'); + $table->index(['tenant_id', 'repo', 'occurred_at'], 'idx_github_contributions_repo_time'); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app-modules/integration-github/database/migrations/2026_06_04_000002_create_github_contributions_table.php` around lines 13 - 32, The migration for the github_contributions table is missing a composite index for repo-scoped temporal queries; inside the Schema::create callback where indexes are added (the closure that defines 'github_contributions'), add a composite index on ['tenant_id','repo','occurred_at'] (e.g. $table->index(['tenant_id','repo','occurred_at'], 'idx_github_contributions_tenant_repo_time')) next to the existing index calls so queries with WHERE tenant_id = ? AND repo = ? ORDER BY occurred_at use the index.app-modules/portal/resources/views/components/retro/slides/highlights.blade.php (1)
2-2: 💤 Low valueNull array key access.
When
$stateisnull, the expression[$state ?? '']will attempt to access the array with an empty string key, which doesn't exist in the map. This will return the fallback'var(--st-open)'correctly, but the logic is unclear. Consider usingmatch($state)with explicit null handling or simplify to$stateColor = fn($state) => match($state) { 'merged' => 'var(--st-merged)', 'open' => 'var(--st-open)', 'closed' => 'var(--st-closed)', default => 'var(--st-open)' }.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app-modules/portal/resources/views/components/retro/slides/highlights.blade.php` at line 2, The current $stateColor arrow closure uses an array lookup with a potentially empty string key (defined as $state ?? ''), which is unclear and can be simplified; update the $stateColor closure (named $stateColor) to use an explicit match expression or a clear switch-style default so null/unknown states map to 'var(--st-open)' — e.g. replace the array lookup with match($state) { 'merged' => 'var(--st-merged)', 'open' => 'var(--st-open)', 'closed' => 'var(--st-closed)', default => 'var(--st-open)' } to make the logic explicit and avoid null/empty-key access.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app-modules/integration-github/src/Backfill/BackfillRepository.php`:
- Around line 110-113: In backfillIssueComments, comments coming from PR
conversations are being attributed as issues because the call to upsert always
uses $this->refFromUrl($this->strv($comment, 'issue_url'), 'issue') and metadata
'kind' => 'issue'; change the logic to check $this->strv($comment,
'pull_request_url') first and if present use
$this->refFromUrl($this->strv($comment, 'pull_request_url'), 'pull_request')
(and set metadata 'kind' => 'pull_request'), otherwise fall back to using
'issue' with issue_url; update the upsert call in backfillIssueComments to pass
the chosen ref and kind while keeping other helpers ($this->strv, $this->userId,
$this->isBot, and ContributionType::Comment) unchanged.
In `@app-modules/integration-github/src/Console/BackfillGithubCommand.php`:
- Around line 52-59: The command currently logs per-repository exceptions in
BackfillGithubCommand (catch blocks using $exception and $throwable) but always
exits with success; modify the execute/handle flow to return a failure exit code
when any repository fails: introduce a boolean flag (e.g., $hadFailures =
false), set it to true inside both catch blocks where you call
$this->error(...), and after processing all repositories return
Symfony\Component\Console\Command\Command::FAILURE if $hadFailures is true
(otherwise return Command::SUCCESS). This ensures non-rate-limit errors cause
the command to exit with a non-zero status.
In `@app-modules/integration-github/src/Contributions/RecordContribution.php`:
- Around line 44-46: The handler currently dispatches GithubContributionRecorded
whenever $emit is true even for updates; change the emit condition to only fire
for newly-created contributions by checking the model creation flag returned by
updateOrCreate—use the returned $contribution->wasRecentlyCreated (or detect
creation before calling updateOrCreate) and only call event(new
GithubContributionRecorded($contribution)) when that flag is true to prevent
re-emitting on edits/replays.
- Around line 33-42: The event dispatch currently emits for both creates and
updates because you call GithubContribution::query()->updateOrCreate(...) and
then unconditionally dispatch when $emit is true; change the logic to only emit
when the model was newly created by checking the returned model's
wasRecentlyCreated property (i.e., after $contribution =
GithubContribution::query()->updateOrCreate(...), only dispatch
GithubContributionRecorded if $emit is true AND
$contribution->wasRecentlyCreated is true) so updates do not double-emit.
In `@app-modules/integration-github/src/Models/GithubContribution.php`:
- Around line 32-63: The GithubContribution model lacks mass-assignment
protection; add an explicit $fillable (or $guarded) property to the
GithubContribution class to whitelist safe attributes (e.g., 'tenant_id',
'type', 'actor_id', 'occurred_at', 'metadata') so calls like create() or
updateOrCreate() cannot set unintended fields; update the class definition near
the existing casts() and newFactory() methods to include the $fillable array (or
set protected $guarded = ['id','created_at','updated_at'] if you prefer a
blacklist approach).
In `@app-modules/integration-github/src/Models/GithubEventLog.php`:
- Around line 23-34: The GithubEventLog model lacks mass-assignment protection;
update the final class GithubEventLog (near the casts() method) to declare
explicit mass-assignment rules by adding either a $fillable array listing
allowed attributes (e.g., 'payload', 'event_type', etc.) or a $guarded array
(e.g., ['id'] or ['*'] as appropriate) to prevent uncontrolled create()/fill()
from assigning unintended attributes; ensure the chosen property is consistent
with other models in the codebase and placed on the class level.
In `@app-modules/integration-github/src/Models/GithubRepository.php`:
- Around line 27-32: The GithubRepository model lacks explicit mass assignment
protection; add either a $fillable array listing permitted attributes (e.g.,
name, owner, url, etc.) or set protected $guarded = [] on the GithubRepository
class to make intent explicit and avoid version-dependent behavior—update the
class definition around the GithubRepository declaration and ensure the chosen
property is declared as protected within the class.
In `@app-modules/integration-github/src/Webhook/GithubWebhookController.php`:
- Around line 19-27: The controller currently reads $event and $delivery and
immediately calls GithubEventLog::query()->firstOrCreate which can persist
invalid dedup keys; update GithubWebhookController to validate the required
GitHub headers ($event from 'X-GitHub-Event' and $delivery from
'X-GitHub-Delivery') before calling GithubEventLog::query()->firstOrCreate: if
$event is empty or $delivery is missing/empty, return an appropriate 4xx
response (or abort) and do not call firstOrCreate; only proceed to create or
dedupe the log when both headers are present and non-empty.
In `@app-modules/integration-github/src/Webhook/VerifyGithubSignature.php`:
- Around line 19-22: The current verification computes an HMAC even when $secret
may be an empty string, allowing forgable signatures; before computing $expected
use the VerifyGithubSignature logic to abort (e.g., abort or throw 403) if
$secret is null/empty/blank, then proceed to compute $expected =
'sha256='.hash_hmac('sha256', $request->getContent(), $secret) and use
hash_equals to compare; ensure the abort path references the same error/response
used for invalid signatures so blank secrets are rejected early.
In `@app-modules/panel-admin/src/Github/Resources/GithubRepositoryResource.php`:
- Around line 61-69: The full_name value must be normalized (lowercased and
trimmed) before persisting so owner/repo casing mismatches don't break
projection/allowlist matching; update the TextInput::make('full_name') form
field to canonicalize state using ->dehydrateStateUsing(fn($state) =>
strtolower(trim((string)$state))) or alternatively implement normalization on
the model (e.g., GithubRepository::setFullNameAttribute) or in
mutateFormDataBeforeSave so saved full_name is always lowercase and trimmed.
In
`@app-modules/portal/resources/views/components/retro/activity-chips.blade.php`:
- Line 11: The view renders $ref['url'] directly into an external href; update
the "activity-chips" Blade component to validate and allowlist URLs before
emitting href: parse the URL (parse_url or filter_var with FILTER_VALIDATE_URL),
ensure the scheme is http or https and the host is in an allowlist (or apply a
safe-host check), and only render href="{{ $ref['url'] }}" when those checks
pass; otherwise omit the href attribute or render a safe fallback; ensure you
still escape the value (e.g., e($url)) when inserting it.
In
`@app-modules/portal/resources/views/components/retro/composition-bar.blade.php`:
- Around line 6-10: The array building in the composition bar uses direct
accesses like $person['prs'], $person['reviews'], $person['issues'],
$person['comments'], and $person['commits'] which will error if a key is
missing; change each count access to use null-coalescing (e.g. $person['prs'] ??
0) so missing keys default to 0 and keep the rest of the array entries (the
color and label fields) unchanged.
In `@app-modules/portal/resources/views/components/retro/deck.blade.php`:
- Around line 64-71: The global `@keydown.window` handler on the component
unconditionally calls $event.preventDefault() for ArrowLeft/Right, which
prevents normal caret/selection behavior when focus is inside form controls;
update the handler in the deck blade so it first checks document.activeElement
(e.g., tagName IN ['INPUT','TEXTAREA','SELECT'] or element.isContentEditable)
and returns early without calling go(...) or preventDefault() if focus is inside
a form control or an editable element, otherwise proceed to call go(active +/-
1) and then preventDefault(); modify the handler surrounding the go and
preventDefault calls to implement this conditional.
- Around line 92-101: The dot and arrow buttons lack accessible names; update
the template x-for button.dot and the nav buttons (class="navbtn") to include
descriptive ARIA attributes: for the dots add a dynamic aria-label like "Go to
slide {i}" and set aria-current (or aria-pressed) when active (use active and i
to compute state) and for the prev/next navbtns add static aria-labels "Previous
slide" and "Next slide" and ensure the :disabled state is reflected with
aria-disabled when appropriate; keep using the existing go(...) handler and the
active/total variables.
In `@app-modules/portal/resources/views/components/retro/filters.blade.php`:
- Around line 37-40: Replace pointer-only spans/divs used as interactive
controls with real interactive elements or make them keyboard-operable: change
the <span class="chip" wire:click="setPreset('...')"> controls to <button
type="button" class="chip" wire:click="setPreset('...')"> for the presets (and
similarly convert other clickable spans/divs in the same file), or if you must
keep non-button elements add role="button" tabindex="0" and a keydown handler
that invokes the same action on Enter/Space. Ensure you update all occurrences
referenced in the comment (the controls that call setPreset via wire:click and
the other clickable chips/toggles) so keyboard users can focus and activate them
and existing styling/ARIA remains intact.
In `@app-modules/portal/resources/views/components/retro/person-card.blade.php`:
- Around line 5-25: The template is reading $person['url'], $person['avatar'],
$person['login'], and $person['total'] directly which will emit warnings if keys
are missing; update person-card.blade.php to defensively access these keys (e.g.
use null-coalescing or data_get) so each usage falls back to a safe default:
href should use ($person['url'] ?? '#'), avatar src should use
($person['avatar'] ?? 'default-avatar.png') and keep the onerror fallback
referencing ($person['login'] ?? 'unknown'), alt should use ($person['login'] ??
'unknown'), and total should default to 0 for the interaction text (use
($person['total'] ?? 0) and apply the singular/plural logic against that value).
Ensure all occurrences of $person['url'], $person['avatar'], $person['login'],
and $person['total'] are updated consistently.
---
Nitpick comments:
In
`@app-modules/integration-github/database/migrations/2026_06_04_000001_create_github_repositories_table.php`:
- Line 15: The migration currently defines the foreign key as
$table->foreignUuid('tenant_id')->constrained('tenants') which leaves delete
behavior as the DB default (RESTRICT); update the migration in
create_github_repositories_table to make the intended behavior explicit: if
repositories should be removed when a tenant is deleted, change the constraint
to use ->cascadeOnDelete() on the tenant_id foreign key, otherwise append
->restrictOnDelete() to document the explicit restrict behavior.
In
`@app-modules/integration-github/database/migrations/2026_06_04_000002_create_github_contributions_table.php`:
- Around line 13-32: The migration for the github_contributions table is missing
a composite index for repo-scoped temporal queries; inside the Schema::create
callback where indexes are added (the closure that defines
'github_contributions'), add a composite index on
['tenant_id','repo','occurred_at'] (e.g.
$table->index(['tenant_id','repo','occurred_at'],
'idx_github_contributions_tenant_repo_time')) next to the existing index calls
so queries with WHERE tenant_id = ? AND repo = ? ORDER BY occurred_at use the
index.
In `@app-modules/integration-github/tests/Feature/GithubContributionTest.php`:
- Around line 39-45: The test creates two GithubContribution records relying on
factory defaults to produce different tenants; make tenant separation explicit
by creating or fetching two distinct tenant entities and passing them into each
factory call (e.g., set a tenant_id or associate a Tenant model) when calling
GithubContribution::factory()->create([...]) so the two creates reference
distinct tenants while keeping repo, type (ContributionType::Pr) and
external_ref the same.
In `@app-modules/integration-github/tests/Feature/GithubRepositoryTest.php`:
- Around line 26-29: The test "permite o mesmo full_name em tenants diferentes"
relies on implicit tenant separation from GithubRepository::factory()->create(),
so make it deterministic by explicitly creating two Tenant instances (e.g.,
$tenantA and $tenantB) and pass/associate each to the repository factory calls
(set tenant_id or use a factory state like forTenant) so the two
GithubRepository::factory()->create([...]) calls create records under different
tenants; update the test to attach the first create to $tenantA and the second
to $tenantB while keeping the same full_name.
In
`@app-modules/portal/resources/views/components/retro/slides/highlights.blade.php`:
- Line 2: The current $stateColor arrow closure uses an array lookup with a
potentially empty string key (defined as $state ?? ''), which is unclear and can
be simplified; update the $stateColor closure (named $stateColor) to use an
explicit match expression or a clear switch-style default so null/unknown states
map to 'var(--st-open)' — e.g. replace the array lookup with match($state) {
'merged' => 'var(--st-merged)', 'open' => 'var(--st-open)', 'closed' =>
'var(--st-closed)', default => 'var(--st-open)' } to make the logic explicit and
avoid null/empty-key access.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Central YAML (inherited)
Review profile: CHILL
Plan: Pro
Run ID: 6277e329-9faa-4414-a723-b904b39c1ff3
⛔ Files ignored due to path filters (1)
public/images/retro-heart.pngis excluded by!**/*.png
📒 Files selected for processing (71)
.env.exampleCONTEXT-MAP.mdapp-modules/integration-github/CONTEXT.mdapp-modules/integration-github/database/factories/GithubContributionFactory.phpapp-modules/integration-github/database/factories/GithubRepositoryFactory.phpapp-modules/integration-github/database/migrations/2026_06_04_000001_create_github_repositories_table.phpapp-modules/integration-github/database/migrations/2026_06_04_000002_create_github_contributions_table.phpapp-modules/integration-github/database/migrations/2026_06_04_000003_create_github_event_logs_table.phpapp-modules/integration-github/docs/adr/0001-github-community-contributions.mdapp-modules/integration-github/routes/github-webhook-routes.phpapp-modules/integration-github/src/Backfill/BackfillRepository.phpapp-modules/integration-github/src/Backfill/RateLimit.phpapp-modules/integration-github/src/Console/BackfillGithubCommand.phpapp-modules/integration-github/src/Contributions/RecordContribution.phpapp-modules/integration-github/src/Enums/ContributionType.phpapp-modules/integration-github/src/Events/GithubContributionRecorded.phpapp-modules/integration-github/src/IntegrationGithubServiceProvider.phpapp-modules/integration-github/src/Models/GithubContribution.phpapp-modules/integration-github/src/Models/GithubEventLog.phpapp-modules/integration-github/src/Models/GithubRepository.phpapp-modules/integration-github/src/Transport/GitHubApiConnector.phpapp-modules/integration-github/src/Transport/Requests/Contributions/GetPullRequest.phpapp-modules/integration-github/src/Transport/Requests/Contributions/ListCommits.phpapp-modules/integration-github/src/Transport/Requests/Contributions/ListIssueComments.phpapp-modules/integration-github/src/Transport/Requests/Contributions/ListIssues.phpapp-modules/integration-github/src/Transport/Requests/Contributions/ListPullRequestReviewComments.phpapp-modules/integration-github/src/Transport/Requests/Contributions/ListPullRequestReviews.phpapp-modules/integration-github/src/Transport/Requests/Contributions/ListPullRequests.phpapp-modules/integration-github/src/Webhook/GithubWebhookController.phpapp-modules/integration-github/src/Webhook/ProjectGithubEvent.phpapp-modules/integration-github/src/Webhook/VerifyGithubSignature.phpapp-modules/integration-github/tests/Feature/BackfillGithubCommandTest.phpapp-modules/integration-github/tests/Feature/BackfillRepositoryTest.phpapp-modules/integration-github/tests/Feature/GithubContributionTest.phpapp-modules/integration-github/tests/Feature/GithubRepositoryTest.phpapp-modules/integration-github/tests/Feature/GithubWebhookTest.phpapp-modules/panel-admin/src/Github/GithubCluster.phpapp-modules/panel-admin/src/Github/Resources/GithubRepositoryResource.phpapp-modules/panel-admin/src/Github/Resources/GithubRepositoryResource/Pages/CreateGithubRepository.phpapp-modules/panel-admin/src/Github/Resources/GithubRepositoryResource/Pages/EditGithubRepository.phpapp-modules/panel-admin/src/Github/Resources/GithubRepositoryResource/Pages/ListGithubRepositories.phpapp-modules/panel-admin/src/PanelAdminServiceProvider.phpapp-modules/panel-admin/tests/Feature/Github/GithubRepositoryResourceTest.phpapp-modules/portal/resources/css/retrospective.cssapp-modules/portal/resources/views/community-retrospective.blade.phpapp-modules/portal/resources/views/components/layouts/deck.blade.phpapp-modules/portal/resources/views/components/retro/activity-chips.blade.phpapp-modules/portal/resources/views/components/retro/badges.blade.phpapp-modules/portal/resources/views/components/retro/composition-bar.blade.phpapp-modules/portal/resources/views/components/retro/deck.blade.phpapp-modules/portal/resources/views/components/retro/filters.blade.phpapp-modules/portal/resources/views/components/retro/person-card.blade.phpapp-modules/portal/resources/views/components/retro/pr-row.blade.phpapp-modules/portal/resources/views/components/retro/slides/closing.blade.phpapp-modules/portal/resources/views/components/retro/slides/community.blade.phpapp-modules/portal/resources/views/components/retro/slides/core.blade.phpapp-modules/portal/resources/views/components/retro/slides/cover.blade.phpapp-modules/portal/resources/views/components/retro/slides/highlights.blade.phpapp-modules/portal/resources/views/components/retro/slides/panorama.blade.phpapp-modules/portal/resources/views/components/retro/slides/repo.blade.phpapp-modules/portal/src/Livewire/CommunityRetrospectivePage.phpapp-modules/portal/src/PortalServiceProvider.phpapp-modules/portal/src/Retrospective/CommunityRetrospective.phpapp-modules/portal/src/Retrospective/RetrospectiveFilters.phpapp-modules/portal/tests/Feature/CommunityRetrospectivePageTest.phpapp-modules/portal/tests/Feature/CommunityRetrospectiveTest.phpapp-modules/portal/tests/Feature/RetrospectiveFiltersTest.phpconfig/he4rt.phpconfig/services.phpdocs/plans/2026-06-04-github-community-contributions.mdvite.config.js
✅ Files skipped from review due to trivial changes (11)
- app-modules/integration-github/src/Enums/ContributionType.php
- app-modules/panel-admin/src/Github/Resources/GithubRepositoryResource/Pages/ListGithubRepositories.php
- app-modules/portal/resources/views/components/retro/slides/cover.blade.php
- app-modules/integration-github/src/Transport/Requests/Contributions/GetPullRequest.php
- app-modules/portal/resources/views/components/retro/slides/closing.blade.php
- config/he4rt.php
- vite.config.js
- app-modules/integration-github/src/Transport/Requests/Contributions/ListPullRequestReviewComments.php
- app-modules/panel-admin/src/Github/GithubCluster.php
- .env.example
- CONTEXT-MAP.md
… repo, seam) - VerifyGithubSignature recusa (500) quando o secret não está configurado, fechando o vetor de webhook forjável com HMAC de chave vazia - full_name canonicalizado em minúsculas (mutator) e o repo do payload normalizado no ProjectGithubEvent, evitando perda silenciosa de eventos e fragmentação por diferença de case - GithubContributionRecorded só dispara na criação (wasRecentlyCreated), evitando re-emissão da seam em edições/replays do webhook
…ReviewComment - escritor único (RecordContribution) recebe um DTO; remove o proxy upsert() - separa review comments em um case próprio do enum (o enum constrói o ref) - helpers coesos (stringFrom/intFrom/actorLogin/actorId) + Str:: no lugar de mb_/str_ends_with/preg_match - ALL_TYPES derivado do enum (remove duplicação) - deck: cor/chip/fatia da DNA para review comments
🔐 Configuração de produção — token, scopes e ENVsPra ligar a ingestão do GitHub em produção precisa de 2 segredos (+1 opcional) e registrar o webhook na org. Sem isso, o backfill pelo painel ainda funciona (autenticado pelo token); só o tempo real (webhook) fica desligado. 1.
|
Reviews PENDING (rascunho não enviado) não têm submitted_at — a API do GitHub lista o rascunho do próprio usuário do token. Gravar occurred_at = "" estourava um QueryException (invalid input syntax for type timestamp) e abortava o backfill. Agora o backfill e o webhook pulam esses reviews. Bug latente, exposto por dados reais.
O backfill faz centenas de requests REST e estourava o timeout no request HTTP do Livewire (cURL error 28). Agora roda em segundo plano. - BackfillGithubRepository (ShouldQueue, ShouldBeUniqueUntilProcessing): executa o backfill no worker; resumível por idempotência; em rate limit faz release() até o reset (não conta como falha); retryUntil 6h + maxExceptions 3 + timeout 600s. - painel "Backfill agora" agora ENFILEIRA o job e volta na hora (notifica "enfileirado"). - RateLimit::secondsUntilReset() para re-agendar o job até o reset. - comando github:backfill segue síncrono (CLI não tem timeout). Requer fila assíncrona + worker (prod=redis). Testes: job (release/stamp) + painel (dispatch).
…CLI) - BackfillRepository::execute ganha um callback opcional de progresso, chamado por contribuição gravada com o ContributionType (seam agnóstico de UI). Job e painel passam null → comportamento idêntico. - github:backfill refinado com Laravel Prompts (intro/table/outro/warning) + barra de progresso ao vivo por repo, com contador por tipo (PRs · reviews · issues · …). - testes: callback chamado por contribuição (na ordem); comando segue cobrindo rate-limit (FAILURE + sem stamp).
Banco zerado / nenhuma contribuição ingerida mostrava 3 slides com tudo "0". Agora, quando não há contribuição alguma para o tenant (count(repoOptions) === 0), o deck renderiza um único slide: coração + "Métricas indisponíveis" + CTA pra reunião semanal (discord.gg/he4rt). O deck entra em modo `bare` (sem progress, FAB de filtros nem navbar). Gatilho é "sem dado nenhum" (não a janela), então uma semana parada com histórico segue caindo no deck normal com filtros.
Por padrão o backfill agora é incremental: com last_backfilled_at preenchido, só busca o que mudou desde (last_backfilled_at − 1 dia). Sem ele (1ª run) ou com --full, varre o histórico inteiro. Mata a varredura completa toda vez. - /issues, /issues/comments, /pulls/comments, /commits → query `since` (server-side) - /pulls (a API não tem `since`) → sort=updated&direction=desc + early-stop no paginate quando updated_at < corte; só os PRs recentes fazem GetPullRequest+reviews - execute(repository, onProgress, full=false); comando ganha --full; painel/job ficam incrementais por padrão (1º backfill é completo, cliques seguintes rápidos) - params `since` validados na doc oficial do GitHub via context7
O que este PR faz
Hoje a comunidade não tem um jeito automático de saber quem está contribuindo nos repositórios públicos. Este PR cria essa base: o sistema passa a guardar as contribuições do GitHub (pull requests, reviews, issues, comentários e commits) e a mostrar tudo numa apresentação de retrospectiva — no espírito do "Quem fez a He4rt bater".
Como funciona, em linguagem simples
1. De onde vêm os dados
2. Quais repositórios contam
3. Quem aparece
A apresentação (deck)
A retrospectiva é uma apresentação em tela cheia, navegável por teclado (← →) ou pelas setas/bolinhas do rodapé, sem o "chrome" do portal. A sequência conta uma história: capa → panorama → núcleo → comunidade → repositórios → destaques → encerramento.
Filtros (aplicados ao vivo)
Um painel de filtros controla o que a apresentação mostra, e tudo fica refletido na URL — dá pra compartilhar um recorte já pronto:
O que muda pra quem usa
Substitui a versão provisória
A primeira tentativa era um comando solto, rodado na mão, que jogava um JSON num link temporário. Isso foi removido e trocado por essa base que vive dentro da arquitetura do projeto, guarda os dados de verdade e se atualiza sozinha.
Para funcionar em produção (configuração)
Precisa configurar duas chaves (
GITHUB_API_TOKENeGITHUB_WEBHOOK_SECRET) e registrar o webhook na organização do GitHub. Sem isso, o histórico pelo painel ainda funciona; só o tempo real fica desligado.